/*-----------------------------------------------------------------------------------*/
//模拟解决线程同步互斥问题（多个线程同时操作文件），通过互斥量、事件来解决，
//主要涉及的内容有多线程的创建，事件的创建，互斥量的创建, 线程的同步
//利用条件变量实现
// 
//设计学生成绩管理系统，主要包含: 
//输入记录模块.查询记录模块.更新记录模块.统计记录模块.输出记录模块

/*
* 操作流程:
* 
* 有全局的数据,就得有与之配套的同步互斥操作,保证全局数据的数据一致性,不能出现有数据竞争导致的数据不一致的情况
* 定义全局的 vector<stu> vector_stu 与之配套的 mutex 和 condition_variable
* 定义全局的 g_file 与之配套的 mutex 和 condition_variable
* 
* 数据先输入 vector_stu(得设置一个input_vector(int thread_id)函数)
* 如果有需要保存的话,再把vector_stu里面的数据保存进入文件 g_file 里面(得设置一个input_file(int thread_id)函数)
* 如果需要查询文件里面某一个的信息的话(得设置一个find_file(int thread_id, string name)函数)
* 如果需要打印文件里面所有人的信息的话(得设置一个output_file(int thread_id)函数)
* 如果需要知道文件里面多少有人的话(得设置一个cnt_file(int thread_id)函数)
* 
*/

#define _CRT_SECURE_NO_WARNINGS 

#include <Windows.h>  // Sleep(1000); 需要的头文件 --> 这里的单位和linux里面的sleep(1)的单位不一样，这里是ms --> 1000ms == 1s
#include <iostream>
#include <stdio.h>
//#include <pthread.h>          // linux 平台(c语言)使用线程的头文件
#include <thread>               // windows 平台(cpp语言封装c语言库的线程)使用线程的头文件
#include <mutex>                // 互斥锁
#include <condition_variable>   // 条件变量

//#include <cstring>
#include <string>
#include <vector>
#include <fstream>
#include <time.h>

using namespace std;

// 默认是 run == 1 是单线程。如果在修改 run != 1 的话，就是多线程
#define run 0
// 设置一个全局的条件编译用的变量 condition 为 1 的话，就是手动输入了
#define condition 0

//菜单选择
void background() {
    cout << "----------1 --> 输入记录模块----------" << endl;
    cout << "----------2 --> 查询记录模块----------" << endl;
    cout << "----------3 --> 更新记录模块----------" << endl;
    cout << "----------4 --> 统计记录模块----------" << endl;
    cout << "----------5 --> 输出记录模块----------" << endl;
    //cout << "----------0 --> 退出----------" << endl;
}

// 全局的 stuv 数组,先输入进数组里面,如果要保存的话,再保存进文件里面
struct stu {
    string name = "";
    int age = 0;
    int yuweng = 0;
    int math = 0;
    int english = 0;
};
vector<stu> vector_stu;
// stuv 不能有数据竞争的情况
std::mutex vector_mtx;
std::condition_variable vector_cv;
bool vector_ready = true;


// 记录多个线程公共的文件 --> 因为有了共享资源 --> 数据竞争 --> 同步于互斥
const string g_file = "g_file.txt";
//const string g_file = "./g_file.txt";  // 这样写测试了，好像也是可以的
// g_file 不能有数据竞争的情况
std::mutex file_mtx;
std::condition_variable file_cv;
bool file_ready = true;


// 1 输入记录模块
// 操作的是 vector_stu 数组(得要有操作的条件的哦)
void input_vector() {
    std::thread::id pthread_id = std::this_thread::get_id();
    cout << "线程" << pthread_id << "在执行任务" << endl;

    //cout << "有人正在写入的情况下是不能先输入进 vector 里面的 " << endl;
    stu s;
    //string name;
    //int age;
    //int yuweng;
    //int math;
    //int english;
    cout << "请输入 name age yuweng math engish 两两之间用空格隔开:";
    
#if(condition == 1)
    // 这个还是得我们自己输入，虽然模拟了几个线程，如果也随机输入的话，那大概是查不到的
    cin >> s.name >> s.age >> s.yuweng >> s.math >> s.english;
#else
    // 线程自己随机输入也是可以的
    int nums = rand() % 100 + 1;
    s.name = to_string(nums);
    
    int cnt = 4;  // 再需要随机输入 4 个成员变量
    while (cnt--) {
        nums = rand() % 100 + 1;
        
        if (s.age == 0) s.age = nums;
        else if (s.yuweng == 0) s.yuweng = nums;
        else if (s.math == 0) s.math = nums;
        else // if (s.age == 0) s.age = nums;
            s.english = nums;
    }
    
#endif

    vector_stu.push_back(s);

    printf("线程 %d 输入完成\n", pthread_id);
}

// 2 查询记录模块 --> 复用模块五的遍历代码
// 操作的是 g_file 文件(得要有操作的条件的哦)
bool find_file(string name, struct stu& s) {
    std::thread::id pthread_id = std::this_thread::get_id();
    cout << "线程" << pthread_id << "在执行任务" << endl;

    //cout << "有人正在写入的情况下是不能查询的 g_file 文件的" << endl;
    bool flag = false;
    //FILE* in = fopen(g_file.c_str(), "r");
    
    // 感觉还是 cpp 的读取一行的方式好写,简单,哈哈哈
    std::ifstream in(g_file);
    if (in.is_open()){

    //if (in != nullptr) {
        // 下面是正确的示范
        //char inbuffer[1024];
        // 因为下面我们要判断 name 是否和读取出来的 inbuffer 第一个字符串匹配要先从 inbuffer 里面截取出来,得用 find + substr 库函数
        // 因为 find 函数得要求 string 类型(好像, 不是很清楚)
        // 再说 substr 函数得需要是 string 类型的, 就把 char inbuffer[1024] 改用 string inbuffer
        //string inbuffer = "";
        //while (fgets(inbuffer.c_str(), inbuffer.size(), in) != nullptr) {
        // 又因为 inbuffer.c_str() 的返回值是一个 const char* 类型的,所以上面的 fgets(inbuffer.c_str(), inbuffer.size(), in)会报错

        //char inbuffer[1024];
        std::string buffer;
        string find_name = "";
        //while (fgets(inbuffer, sizeof(inbuffer) - 1, in)) {
        while(std::getline(in, buffer)){
            //cout << inbuffer << endl;
            //cnt++;
            
            // string 和 char类型的数组是可以相互转化的哦
            //buffer = inbuffer;
            //cout << buffer << endl;  // debug 用的
            // 找到第一个 ' ' 的位置,就是 pos 的指向的位置
            auto pos = buffer.find(' ');   // 传 " " 是有问题的, 因为本来就是找一个字符的
            if (pos == std::string::npos)
                cout << "debug" << endl; // 一定不会的 --> 读取出来的格式是一定是 "name age yuweng math engish" 
            // 下面的这一行是有问题的,因为外面定义了 find_name
            //string find_name = buffer.substr(0, pos - buffer.begin()/*指针减指针,就是指针之间的距离*/);
            
            // 下面的这一行也是有问题的 因为 substr 的第二个参数就是 pos
            //find_name = buffer.substr(0, pos - buffer.begin() - 1/*指针减指针,就是指针之间的距离*/);
            
            find_name = buffer.substr(0, pos);
            if (find_name == name) {
                flag = true;
                break;  // 找到了
            }
        }
        // 找到了,就把 inbuffer 给 s, 传递出去
        if (flag == true) {
            // 切割 string buffer = "name age yuweng math engish";
            s.name = name;
            for (int i = name.size() + 1; i < buffer.size(); i++) {
                string str = "";
                // 下面的 i < buffer.size() 得加上的哦,总以为是上面的 substr 函数的问题,找了半天,唉....
                while(i < buffer.size() && buffer[i] != ' ')
                    str += buffer[i++];
                
                // 为了简化下面的操作 --> struct stu s; 成员变量初始化都是 0
                // 下面的, 每一次从 buffer 里面截取一个数字字符串, 只执行下面其中的一行的哦
                if (s.age == 0) s.age = stoi(str);
                else if (s.yuweng == 0) s.yuweng = stoi(str);
                else if (s.math == 0) s.math = stoi(str);
                else // (s.english == 0) 
                    s.english = stoi(str);
            }
        }

        //fclose(in);  // c 语言里面的关闭文件指针的操作是这样写的
        in.close();
    }
    else {
        // 文件打开失败
        std::cout << "Error opening file" << endl;
    }

    return flag;
}

// 3 更新记录模块
// 操作的是 vector_stu 数组 和 g_file 文件(得要有操作的条件的哦)
void input_file() {
    std::thread::id pthread_id = std::this_thread::get_id();
    cout << "线程" << pthread_id << "在执行任务" << endl;

    //cout << "把vector里面的数据保存进 g_file 文件里面" << endl;
    ofstream out;
    //std::ofstream::app 表示的是追加式的输入到文件里面(app --> append 追加的意思)
    out.open(g_file.c_str(), std::ofstream::out | std::ofstream::app);
    // 判断文件是否正确打开了
    if (out.is_open()) {
        for (int i = 0; i < vector_stu.size(); i++) {
            //char inbuffer[1024];
            //snprintf(inbuffer, sizeof(inbuffer), "%s %d %d %d %d",
                //vector_stu[i].name, vector_stu[i].age, vector_stu[i].yuweng, vector_stu[i].math, vector_stu[i].english);

            string inbuffer = "";
            inbuffer += vector_stu[i].name;
            inbuffer += " ";
            inbuffer += to_string(vector_stu[i].age);
            inbuffer += " ";
            inbuffer += to_string(vector_stu[i].yuweng);
            inbuffer += " ";
            inbuffer += to_string(vector_stu[i].math);
            inbuffer += " ";
            inbuffer += to_string(vector_stu[i].english);
            inbuffer += " ";

            out << inbuffer.c_str() << endl;
        }
        
        // 数组中的数据添加进文件中了,就得清空数组
        vector_stu.clear();

        //close(out);  // 不是这样写的
        out.close();
    }
    else {
        // 文件打开失败
        std::cout << "Error opening file" << endl;
    }

    cout << "vector_stu 中的数据加载到文件 g_file 中已完成\n";
}

//4 统计记录模块 --> 复用模块五的遍历代码
// 操作的是 g_file 文件(得要有操作的条件的哦)
int cnt_file() {
    std::thread::id pthread_id = std::this_thread::get_id();
    cout << "线程" << pthread_id << "在执行任务" << endl;

    //cout << "统计记录模块需要做什么呢?  先统计g_file已经记录了多少个stu的信息吧" << endl;
    // 用 c 语言的方式,读取文件,一次读取一行
    // 有多少行就表示有多少个人人的信息
    int cnt = 0;
    FILE* in = fopen(g_file.c_str(), "r");
    if (in != nullptr) {
        // 下面是正确的示范
        char inbuffer[1024];
        while (fgets(inbuffer, sizeof(inbuffer) - 1, in) != nullptr) {
            //cout << inbuffer << endl;
            cnt++;  // 计数
        }

        fclose(in);  // c 语言里面的关闭文件指针的操作是这样写的
    }
    else {
        // 文件打开失败
        std::cout << "Error opening file" << endl;
    }

    return cnt;
}

// 5 输出记录模块
// 操作的是 g_file 文件(得要有操作的条件的哦)
void output_file() {
    std::thread::id pthread_id = std::this_thread::get_id();
    cout << "线程" << pthread_id << "在执行任务" << endl;

    //cout << "把g_file 文件里面的数据输出出来" << endl;
    
    // 用 cpp 语言的方式,读取文件 
    // --> 因为一次读取的是一个字符,显示不好看,所以采用c的方式(当然cpp的getline函数也可以一次读取一行的)
    //std::ifstream in;
    //in.open(g_file.c_str(), std::ifstream::in);
    //if (in.is_open()) {
    //    // 看用的是 char 接收的返回值的 --> 一次读取的是一个字符的哦
    //    char c = in.get();   
    //    while (in.good()) {
    //        std::cout << c;
    //        
    //        c = in.get();
    //    }

    //    //close(in);  // 不是这样写的
    //    in.close();
    //}
    //else {
    //    // 文件打开失败
    //    std::cout << "Error opening file" << endl;
    //}


    // 用 c 语言的方式,读取文件,一次读取一行
    FILE* in = fopen(g_file.c_str(), "r");
    if (in != nullptr) {
        // 我们想要的是一次读取一行的哦 --> 反而有一点点的问题 --> 用上面的还没有问题
        
        // 下面是错误的示范
        //char inbuffer[1024];
        //fgets(inbuffer, sizeof(inbuffer) - 1, in);
        //while (in != nullptr) {
        //    // 不然下面输出的时候得碰到 \0 才停止输出
        //    inbuffer[strlen(inbuffer) - 1] = 0;
        //    std::cout << inbuffer;

        //    fgets(inbuffer, sizeof(inbuffer) - 1, in);
        //}

        // 下面是正确的示范
        char inbuffer[1024];
        while (fgets(inbuffer, sizeof(inbuffer) - 1, in) != nullptr) {
            cout << inbuffer;
        }

        fclose(in);  // c 语言里面的关闭文件指针的操作是这样写的
    }
    else {
        // 文件打开失败
        std::cout << "Error opening file" << endl;
    }
}



// 所有的用户(线程模拟)都需要跑这个函数
void* thread_run(void* args) {

    background();
    //system("cls");  // 清屏的功能
    //system("pause");  // 停顿的功能

    int i = 0;
    do{
        cout << "请输入选项: ";
#if(condition == 1)
        cin >> i;  // 手动输入 debug
#else
        // 输入 0 表示的是退出的意思，+1 --> [1, 5] --> 不让线程们退出
        i = rand() % 5 + 1;
        //scanf("%d", &i);
#endif
        switch (i){
        case 1:
        {
            // 输入记录模块
            // 操作的是 vector_stu 数组(得要有操作的条件的哦)
            //有人正在写入的情况下是不能输入(数据竞争)的 g_file 文件的

            std::unique_lock<std::mutex> lck(vector_mtx);
            while (!vector_ready) {
                vector_cv.wait(lck);
                cout << "线程正在等待资源中......" << endl;
            }
            // ...  线程去执行任务
            // 执行完了 RAII 锁 lck 自动销毁了 --> 局部变量 lck 出了作用域自动销毁

            input_vector();
            Sleep(1000);
        }
            break;
        case 2:
        {
            // 查询记录模块
            // 操作的是 g_file 文件(得要有操作的条件的哦)
            //有人正在写入的情况下是不能查询(数据竞争)的 g_file 文件的
            
            std::unique_lock<std::mutex> lck(file_mtx);
            while (!file_ready) {
                vector_cv.wait(lck);
                cout << "线程正在等待资源中......" << endl;
            }
            // ...  线程去执行任务
            // 执行完了 RAII 锁 lck 自动销毁了 --> 局部变量 lck 出了作用域自动销毁

            string name;
#if(condition == 1)
            // 这个还是得我们自己输入，虽然模拟了几个线程，如果也随机输入的话，那大概是查不到的
            cout << "请输入要查询的人的名字: ";  
            cin >> name;
#else
            // 随机输入
            int nums = rand() % 100 + 1;
            name = to_string(nums);
#endif
            struct stu s;   // 把要查找的人的信息,通过下面的这个函数给带出来
            bool flag = find_file(name, s);
            if (flag == true)
                cout << "找到了: " << s.name << ' ' << s.age << ' ' << s.yuweng << " " << s.math << ' ' << s.english << endl;
            else if (flag == false)
                cout << "没有找到这个人的信息" << endl;

            // 这里好像不能开一个全局的 unordered_map 开存储每一次存储进入文件中的人的名字
            // 因为真实情况中每一个人进入这个系统的时间不一样 .........
            // --> 应该可以用 unordered_map 咱们不是具体做搜索的,哈哈哈哈. 就直接遍历文件判断了
            
            Sleep(1000);
        }
            break;
        case 3:
        {
            // 更新记录模块
            // 操作的是 vector_stu 数组 和 g_file 文件(得要有操作的条件的哦)
            
            // 这里用了两把锁(注意加锁的顺序), 感这里只有这一个函数需要加两把锁,应该没有死锁的情况
            std::unique_lock<std::mutex> file_lck(file_mtx);
            std::unique_lock<std::mutex> vector_lck(vector_mtx);
            while (!file_ready && !vector_ready) {
                vector_cv.wait(vector_lck);
                cout << "线程正在等待资源中......" << endl;
            }
            // ...  线程去执行任务
            // 执行完了 RAII 锁 lck 自动销毁了 --> 局部变量 lck 出了作用域自动销毁

            input_file();

            Sleep(1000);
        }
            break;
        case 4:
        {
            // 统计记录模块
            // 操作的是 g_file 文件(得要有操作的条件的哦)

            std::unique_lock<std::mutex> lck(file_mtx);
            while (!file_ready) {
                vector_cv.wait(lck);
                cout << "线程正在等待资源中......" << endl;
            }
            // ...  线程去执行任务
            // 执行完了 RAII 锁 lck 自动销毁了 --> 局部变量 lck 出了作用域自动销毁

            int cnt = cnt_file();
            // 看 cnt_file 函数的实现的话 --> 返回是 0 的话是有两种情况的哦
            printf("g_file 文件里面已经存储了 %d 个人的信息\n", cnt);
            
            Sleep(1000);
        }
            break;
        case 5:
        {
            // 输出记录模块
            // 操作的是 g_file 文件(得要有操作的条件的哦)

            std::unique_lock<std::mutex> lck(file_mtx);
            while (!file_ready) {
                vector_cv.wait(lck);
                cout << "线程正在等待资源中......" << endl;
            }
            // ...  线程去执行任务
            // 执行完了 RAII 锁 lck 自动销毁了 --> 局部变量 lck 出了作用域自动销毁

            output_file();
            
            Sleep(1000);
        }
            break;
        default:
            cout << "请输入[0, 5] 之间的整数!" << endl;
            break;
        }

        //退出
    } while (i != 0);
 
    return nullptr;
}

// main 函数充当主线程的作用 --> 
int main(void) {
    srand((long int)time(nullptr));

    // 默认是 run == 1 是单线程。如果在修改 run != 1 的话，就是多线程
#if(run == 1)
    // 先可以创建一个线程先，看看能不能使用。--> 单线程不存在死锁的问题（有，但这里应该没有）
    // 这里用了两把锁(注意加锁的顺序), 感这里只有这一个函数需要加两把锁,应该没有死锁的情况
    // 默认是 condition == 0 是线程自动输入。如果在修改 condition == 1 的话，就是手动输入了
    int i = 1;
    thread t(thread_run, &i);
    t.join();

#else
    //多线程情况下判定死锁的做法：设置 condition == 0，看足够长的时间内，死循环会不会停下来。
          // 如果停止了，说明线程之间相互竞争互斥锁导致死锁了。如果足够长的时候没有停下来，说明没有太大的翁提
    // 创建 thread_cnt 个线程,thread_cnt 必须得是常量const --> int thread_cnt = 3; 就不行的哦
    const int thread_cnt = 5;
    thread threads[thread_cnt];

    // 所有的用户(线程模拟)都需要跑 thread_run 函数
    for (int i = 0; i < thread_cnt; i++)
        threads[i] = thread(thread_run, &i);  

    // main 函数作为主线程回收子线程
    for (auto& th : threads) th.join();
#endif

    return 0;
}


/*
相关文章可百度：

推荐一篇关于多线程/多进程下有多把锁的情况下怎么防止死锁：
《多线程时需要注意互斥锁的加锁顺序，防止死锁》
https://blog.csdn.net/weixin_43354152/article/details/127365143

*/


